import { _decorator, CCFloat, CCInteger, EventMouse, EventTouch, instantiate, Mask, Node, NodeEventType, Prefab, tween, Tween, UITransform, Widget } from "cc";
import { BaseComp } from "../base/BaseComp";
import { ScrollListCell } from "./ScrollListCell";
const { ccclass, property, executeInEditMode } = _decorator;

/**
 * 滚动列表
 * 对长数据优化,只会生成可见范围内数量的单元格
 * 设置对应的回调来进行渲染单元格、选中操作、或者滚动逻辑
 */
@ccclass("ScrollList")
@executeInEditMode
export class ScrollList<T = any> extends BaseComp {

    @property({ tooltip: "是否显示预览,预览完关闭提高加载性能" })
    get showPrev() { return this._showPrev; }
    set showPrev(v) { this._showPrev = v; this.updatePrev(); }
    private _showPrev = false;
    @property({ visible: true })
    private _isVector = true;
    @property({ type: CCInteger, visible: true })
    private _repeatX = 0;
    private _realRepeatX: number;
    private _isAutoRepeatX: boolean;
    @property({ type: CCInteger, visible: true })
    private _repeatY = 0;
    private _realRepeatY: number;
    private _isAutoRepeatY: boolean;
    @property({ type: CCFloat, visible: true, step: 1 })
    private _spaceX = 0;
    @property({ type: CCFloat, visible: true, step: 1 })
    private _spaceY = 0;
    @property({ type: CCFloat, visible: true, step: 1 })
    private _elastic = 0;
    private _realElastic = 0;
    @property({ type: CCFloat, visible: true, step: 1 })
    private _elasticTime = 0.1;
    @property({ type: CCFloat, visible: true, step: 0.1 })
    private _inertiaTime = 1;
    @property({ type: CCFloat, visible: true, step: 0.1 })
    private _inertiaNeedMaxTime = 0.3;
    @property({ type: CCFloat, visible: true, step: 1 })
    private _inertiaNeedDis = 15;
    @property({ type: Prefab, visible: true })
    private _prefab: Prefab

    /** 列表的变换组件 */
    trans: UITransform;
    /** 实际内容节点 */
    contNode: Node;
    /** 实际内容节点的变换组件 */
    contTrans: UITransform;
    /** 列表是否正在拖拽中 */
    isDraging: boolean;
    /**
     * 单元格渲染回调 会在列表赋值时、选中(当前选中、上次选中)、滚动时(发生滚动单元格交换)触发
     * (赋值时非箭头函数需要bind(caller) 防止this指向错误)
     * @param cell 触发渲染的单元格
     */
    renderCallback: (cell: ScrollListCell<T>) => void;
    /**
     * 单元格选中回调 触发选中时执行
     * (赋值时非箭头函数需要bind(caller) 防止this指向错误)
     * @param idx 当前选中的单元格index
     * @param lastIdx 上次选中的单元格index
     */
    selectCallback: (idx: number, lastIdx: number) => void;
    /**
     * 滚动回调 触发滚动时执行
     * (赋值时非箭头函数需要bind(caller) 防止this指向错误)
     * @param val 当前滚动值
     * @param delta 与上次滚动值的插值
     */
    scrollCallback: (val: number, delta: number) => void;

    private _needLayout = true;
    private _cellW: number;
    private _cellH: number;
    private _cellWSp: number;
    private _cellHSp: number;
    private _cellMax: number;
    private _scrollVal = 0;
    private _scrollValMax = 0;
    private _scrollTween: { val: number };
    private _startIdx: number;
    private _selectedIdx = -1;
    private _list: T[];
    private _len = 0;
    private _cells: ScrollListCell<T>[] = [];
    private _cellPool: ScrollListCell<T>[] = [];

    onLoad() {
        if (!this._prefab) throw new Error(`ScrollList(${this.name})组件没有挂载prefab`);
        let node = instantiate(this._prefab);
        if (node.getComponent(Widget)) throw new Error(`ScrollList(${this.name})组件的prefab(${node.name})不允许挂载Widget组件`);
        let trans = node.getComponent(UITransform);
        if (!trans) throw new Error(`ScrollList(${this.name})组件的prefab(${node.name})没有挂载UITransform`);

        this._cellW = trans.width;
        this._cellH = trans.height;
        node.destroy();

        this._isAutoRepeatX = !this.repeatX;
        this._isAutoRepeatY = !this.repeatY;

        !this.getComponent(Mask) && this.addComponent(Mask);
        this.trans = this.getComponent(UITransform);
        this.contNode = this.node.getChildByName("Content");
        if (!this.contNode) {
            this.node.addChild(this.contNode = new Node("Content"));
            this.contNode.layer = this.node.layer;
            this.contTrans = this.contNode.addComponent(UITransform);
            this.contTrans.setAnchorPoint(0, 1);
            this.contNode.addComponent(Mask);
            let contWidget = this.contNode.addComponent(Widget);
            contWidget.isAlignLeft = contWidget.isAlignTop = true;
            contWidget.left = contWidget.top = 0;
        } else {
            this.contNode.destroyAllChildren();
            this.contTrans = this.contNode.getComponent(UITransform);
        }

        this.elasticTime = this._elasticTime;
        Object.defineProperty(this._scrollTween = {} as any, "val", {
            get: () => this._scrollVal,
            set: (v: number) => this.scrollVal = v
        });

        this.scrollEvent();
    }

    lateUpdate() {
        this.checkLayout();
    }

    onDestroy() {
        Tween.stopAllByTarget(this._scrollTween);
        for (let i = this._cellPool.length; --i >= 0;) {
            this._cellPool[i].destroy();
        }
    }

    /** x方向最大单元格数量 0为自适应 */
    get repeatX() { return this._repeatX; }
    set repeatX(v: number) {
        this._repeatX = v;
        this._isAutoRepeatX = !v;
        this._needLayout = true;
    }

    /** y方向最大单元格数量 0为自适应 */
    get repeatY() { return this._repeatY; }
    set repeatY(v: number) {
        this._repeatY = v;
        this._isAutoRepeatY = !v;
        this._needLayout = true;
    }

    /** 超出边界拖动时的橡皮筋距离 */
    get elastic() { return this._realElastic; }
    set elastic(v: number) {
        this._elastic = Math.max(0, v);
        this._realElastic = v || (this._isVector ? this._cellH : this._cellW);
    }

    /** 橡皮筋回弹时间 */
    get elasticTime() { return this._elasticTime; }
    set elasticTime(v: number) {
        this._elasticTime = Math.max(0, v);
    }

    /** 滚动值 */
    get scrollVal() { return this._scrollVal; }
    set scrollVal(v: number) {
        let last = this._scrollVal;
        this._scrollVal = Math.max(-this._realElastic, Math.min(this._scrollValMax + this._realElastic, v));
        this.updatePos();
        this.scrollCallback && this.scrollCallback(this._scrollVal, this._scrollVal - last);
    }

    /** 当前选中的单元格index 重复设置只会触发一次选中和相关单元格的渲染回调 */
    get selectedIdx() { return this._selectedIdx; }
    set selectedIdx(idx: number) {
        if (idx != this._selectedIdx) {
            this.setSelectedIdx(idx);
        }
    }

    /** 当前选中的单元格数据 */
    get selection() { return this._list[this._selectedIdx]; }
    /** 数据源长度(单元格个数) */
    get len() { return this._len; }
    /** 最大容纳多少个单元格 会算上作为滑动缓冲的一行/列的数量 */
    get cellMax() { return this._cellMax; }
    /** 显示中的单元格列表 */
    get cells() { return this._cells; }

    /** 列表数据源 赋值时会刷新列表 */
    get list() { return this._list; }
    set list(list: T[]) {
        this._list = list;
        this._len = this._list.length;
        this.refresh();
    }

    /**
     * 获取单元格 当不在显示范围内时为空
     * @param idx 
     */
    getCell(idx: number) {
        return this._cells[idx - this._startIdx];
    }

    /**
     * 获取显示中的单元格
     * @param idx 
     */
    getShowCell(idx: number) {
        return this._cells[idx];
    }

    /**
     * 设置单元格数据
     * @param idx 
     * @param data 
     */
    setCellData(idx: number, data: T) {
        this._list[idx] = data;
        let cell = this.getCell(idx);
        this.doCellRender(cell);
    }

    /**
     * 设置当前选中的单元格index 即使重复设置也会触发选中和相关单元格的渲染回调
     * @param idx 
     */
    setSelectedIdx(idx: number, scrollTo?: boolean, scrollTime?: number) {
        let last = this._selectedIdx,
            cell = this.getCell(idx),
            lastCell = this.getCell(last);
        this._selectedIdx = idx;
        scrollTo && this.scrollTo(idx, scrollTime);
        this.selectCallback && this.selectCallback(idx, last);
        cell && cell.onSelect();
        this.doCellRender(cell);
        this.doCellRender(lastCell);
    }

    /**
     * 滚动到某个单元格
     * @param idx 
     * @param time 滚动时间(秒)
     */
    scrollTo(idx: number, time?: number) {
        let val = Math.max(0, Math.min(this._scrollValMax, this._isVector ?
            (idx / this.repeatX >> 0) * this._cellHSp :
            (idx / this.repeatY >> 0) * this._cellWSp));
        this.stopScroll();
        if (time > 0) tween(this._scrollTween).to(time, { val }, { easing: "quadOut" }).start();
        else this.scrollVal = val;
    }

    /**
     * 停止滚动
     */
    stopScroll() {
        Tween.stopAllByTarget(this._scrollTween);
    }

    /**
     * 立即刷新布局
     */
    layout() {
        this._needLayout = true;
        this.checkLayout();
        this.refresh();
    }

    /**
     * 刷新列表
     */
    refresh() {
        this.checkLayout();
        let cellLen = this._cells.length;
        if (cellLen > this._len) {
            cellLen -= this._len;
            let cells = this._cells.splice(this._len, cellLen);
            for (let i = cellLen; --i >= 0;) {
                this.recyCell(cells[i]);
            }
        } else {
            for (let i = Math.min(this._cellMax - cellLen, this._len - cellLen); --i >= 0;) {
                this._cells.push(this.createCell());
            }
        }
        this._scrollValMax = this._isVector ?
            Math.max(0, (Math.ceil(this._len / this._realRepeatX) * (this._cellH + this._spaceY)) - this._spaceY - this.contTrans.height) :
            Math.max(0, (Math.ceil(this._len / this._realRepeatY) * (this._cellW + this._spaceX)) - this._spaceX - this.contTrans.width);
        this.updatePos(true);
    }

    private updatePos(refresh = false) {
        this.checkLayout();
        let startIdx = Math.max(0, this._isVector ?
            (this._scrollVal / this._cellHSp >> 0) * this._realRepeatX :
            (this._scrollVal / this._cellWSp >> 0) * this._realRepeatY);

        if (startIdx !== this._startIdx) {
            if (this._startIdx !== undefined) {
                let diff = (startIdx - this._startIdx) * (this._isVector ? this._realRepeatX : this._realRepeatY);
                if (diff < 0) diff += this._cells.length;
                this._cells = this._cells.slice(diff).concat(this._cells.slice(0, diff));
            }
            this._startIdx = startIdx;
        }

        if (this._isVector) {
            let startY = this._scrollVal > 0 ? this._scrollVal % this._cellHSp : this._scrollVal;
            for (let i = this._cells.length; --i >= 0;) {
                let cell = this._cells[i],
                    idx = this._startIdx + i,
                    doRender = refresh;
                if (cell.idx !== idx) {
                    cell.idx = idx;
                    doRender = true;
                }
                if (idx < this.len) {
                    cell.posNode.active = true;
                    cell.showIdx = i;
                    cell.posNode.setPosition(this._cellWSp * (i % this._realRepeatX), startY - this._cellHSp * (i / this._realRepeatX >> 0));
                    doRender && this.doCellRender(cell);
                } else {
                    cell.posNode.active = false;
                }
            }
        } else {
            let startX = -(this._scrollVal > 0 ? this._scrollVal % this._cellWSp : this._scrollVal);
            for (let i = this._cells.length; --i >= 0;) {
                let cell = this._cells[i],
                    idx = this._startIdx + i,
                    doRender = refresh;
                if (cell.idx !== idx) {
                    cell.idx = idx;
                    doRender = true;
                }
                if (idx < this.len) {
                    cell.posNode.active = true;
                    cell.showIdx = i;
                    cell.posNode.setPosition(startX + this._cellWSp * (i / this._realRepeatY >> 0), -this._cellHSp * (i % this._realRepeatY));
                    doRender && this.doCellRender(cell);
                } else {
                    cell.posNode.active = false;
                }
            }
        }
    }

    private createCell() {
        let cell = this._cellPool.pop();
        if (!cell) {
            let node = instantiate(this._prefab);
            if (this._showPrev) {
                //预览模式
                cell = node.getComponent(ScrollListCell) as any;
                cell && cell.destroy();
                cell = node.addComponent(ScrollListCell);
            } else {
                cell = node.getComponent(ScrollListCell) as any || node.addComponent(ScrollListCell);
            }
            cell.onCreate(this);
        }
        cell.idx = undefined;
        this.contNode.addChild(cell.posNode);
        return cell;
    }

    private recyCell(cell: ScrollListCell<T>) {
        this._cellPool.push(cell.onRecy());
    }

    private doCellRender(cell: ScrollListCell<T>) {
        if (cell) {
            cell.onRender();
            this.renderCallback && this.renderCallback(cell);
        }
    }

    private checkLayout() {
        if (this._needLayout) {
            this._needLayout = false;
            this.elastic = this._elastic;
            this._cellWSp = this._cellW + this._spaceX;
            this._cellHSp = this._cellH + this._spaceY;
            this._repeatX = Math.max(1, this._repeatX || (this.trans.width + this._spaceX) / this._cellWSp);
            this._repeatY = Math.max(1, this._repeatY || (this.trans.height + this._spaceY) / this._cellHSp);
            this._realRepeatX = this._repeatX = this._isAutoRepeatX && !this._isVector ? Math.ceil(this._repeatX) : (this._repeatX >> 0);
            this._realRepeatY = this._repeatY = this._isAutoRepeatY && this._isVector ? Math.ceil(this._repeatY) : (this._repeatY >> 0);
            this._isVector ? ++this._realRepeatY : ++this._realRepeatX;
            this._cellMax = this._realRepeatX * this._realRepeatY;
            this.contTrans.setContentSize(
                this._isAutoRepeatX ? this.trans.width : (this._repeatX * this._cellWSp - this._spaceX),
                this._isAutoRepeatY ? this.trans.height : (this._repeatY * this._cellHSp - this._spaceY)
            );
        }
    }

    /**
     * 刷新预览(编辑器显示)
     */
    private updatePrev() {
        this.contNode.destroyAllChildren();
        if (this._showPrev) {
            this._needLayout = true;
            this.checkLayout();
            this._len = this._cellMax;
            this.refresh();
            this._len = 0;
        }
        this._cells = [];
        this._cellPool = [];
    }

    // =========== 滚动逻辑 ===========

    private _dragStartTime: number;
    private _dragDelta: number;
    private _wheelStartTime: number;
    private _wheelVal: number;
    private _wheelDelta: number;

    private scrollEvent() {
        this.node.on(NodeEventType.TOUCH_START, (e: EventTouch) => {
            this.isDraging = false;
            this._dragStartTime = Date.now();
            this._dragDelta = 0;
            this.clearTimer(this.dragEndCallback);
            this.stopScroll();
        }, this);

        this.node.on(NodeEventType.TOUCH_MOVE, (e: EventTouch) => {
            let delta = this._isVector ? e.getDeltaY() : -e.getDeltaX();
            if (this._scrollVal < 0 || this._scrollVal > this._scrollValMax) {
                //超出边界增加阻力
                delta *= 0.5;
            }
            this.scrollVal += delta;
            this._dragDelta += delta;
            if (Math.abs(this._dragDelta) > 5) this.isDraging = true;
        }, this);

        this.node.on(NodeEventType.TOUCH_END, this.dragEnd, this);
        this.node.on(NodeEventType.TOUCH_CANCEL, this.dragEnd, this);

        this._wheelStartTime = -1;
        this.node.on(NodeEventType.MOUSE_WHEEL, (e: EventMouse) => {
            let delta = (this._isVector ? -e.getScrollY() : e.getScrollY()) * 0.1;
            if (this._wheelStartTime === -1) {
                this._wheelStartTime = Date.now();
                this._wheelDelta = delta;
                this._wheelVal = this._scrollVal + delta;
            } else {
                this._wheelDelta += delta;
                this._wheelVal += delta;
            }
            this.stopScroll();
            tween(this._scrollTween)
                .to(0.1, { val: this._wheelVal })
                .call(this.wheelEndCallback.bind(this))
                .start();
        }, this);
    }

    private dragEnd() {
        this.callLater(this.dragEndCallback);
        this.checkInertia(this._dragDelta, (Date.now() - this._dragStartTime) / 1000);
    }

    private dragEndCallback() {
        this.isDraging = false;
    }

    private wheelEndCallback() {
        this.checkInertia(this._wheelDelta, Math.max(0.233, (Date.now() - this._wheelStartTime) / 1000));
        this._wheelStartTime = -1;
    }

    private checkInertia(delta: number, time: number) {
        let dis = Math.abs(delta);
        if (this._inertiaTime > 0 && time < this._inertiaNeedMaxTime && dis > this._inertiaNeedDis) {
            this.stopScroll();
            let len = this._isVector ? this.contTrans.height : this.contTrans.width;
            let change = Math.sign(delta) * Math.min(len * 3, this._inertiaTime * dis / time);
            let val = this._scrollVal + change;
            tween(this._scrollTween)
                .to(this._inertiaTime, { val }, {
                    easing: "quadOut",
                    onUpdate: this.checkElastic.bind(this),
                })
                .call(this.checkElastic.bind(this))
                .start();
        } else {
            this.checkElastic();
        }
    }

    private checkElastic() {
        if (this._scrollVal < 0) {
            this.stopScroll();
            tween(this._scrollTween).to(this._elasticTime, { val: 0 }, { easing: "quadOut" }).start();
        } else if (this._scrollVal > this._scrollValMax) {
            this.stopScroll();
            tween(this._scrollTween).to(this._elasticTime, { val: this._scrollValMax }, { easing: "quadOut" }).start();
        }
    }

}